<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Torch Flame</title>
  <style>
    body, html { margin: 0; padding: 0; background: black; overflow: hidden; }
    canvas { width: 100vw; height: 100vh; display: block; }
  </style>
</head>
<body>
<canvas id="glcanvas"></canvas>
<script>
const vertexShaderSource = `
attribute vec2 a_position;
varying vec2 v_uv;
void main() {
    v_uv = a_position * 0.5 + 0.5;
    gl_Position = vec4(a_position, 0.0, 1.0);
}
`;

const fragmentShaderSource = `

precision mediump float;

out vec4 fragColor;
uniform vec2 uRes;
uniform float uTime;
uniform sampler2D txt;

// ------------------ helpers ------------------
vec2 random2(vec2 st) {
    st = vec2(dot(st, vec2(127.1, 311.7)), dot(st, vec2(269.5, 183.3)));
    return -1.0 + 2.0 * fract(sin(st) * 43758.5453123);
}

float noiseGradient(vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);
    vec2 u = f * f * (3.0 - 2.0 * f);

    float a = dot(random2(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0));
    float b = dot(random2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
    float c = dot(random2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
    float d = dot(random2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));

    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

float fbm(vec2 p) {
    float v = 0.0;
    float a = 0.5;
    for (int i = 0; i < 5; i++) {
        v += a * noiseGradient(p);
        p *= 2.0;
        a *= 0.5;
    }
    return v;
}

// palette: white -> yellow -> orange -> red -> dark
vec3 firePalette(float t) {
    t = clamp(t, 0.0, 1.0);
    vec3 c1 = mix(vec3(1.0, 1.0, 1.0), vec3(1.0, 0.9, 0.5), smoothstep(0.0, 0.3, t)); // white -> pale yellow
    vec3 c2 = mix(vec3(1.0, 0.8, 0.2), vec3(1.0, 0.4, 0.0), smoothstep(0.3, 0.6, t)); // yellow -> orange
    vec3 c3 = mix(vec3(0.9, 0.25, 0.05), vec3(0.2, 0.05, 0.02), smoothstep(0.6, 1.0, t)); // orange -> dark red
    if (t < 0.3) return mix(c1, vec3(1.0,0.9,0.5), t/0.3);
    if (t < 0.6) return mix(vec3(1.0,0.9,0.5), c2, (t-0.3)/0.3);
    return mix(c2, c3, (t-0.6)/0.4);
}

// ------------------ main ------------------
void main() {
    // normalized uv: (0..1)
    vec2 uv = gl_FragCoord.xy / uRes;

    // adjust aspect so x is scaled to preserve shape
    vec2 aspect = vec2(uRes.x / uRes.y, 1.0);
    vec2 p = (uv - vec2(0.5, 0.12)) * vec2(aspect.x, 1.8); // center the torch near bottom center, stretch vertically

    // initial "seed" bright spot when time <= 0 (keeps your original behavior)
    vec3 smoke = vec3(0.0);
    if (uTime <= 0.0) {
        vec2 st = uv - vec2(0.5, 0.12);
        st.x *= uRes.x / uRes.y;
        float len = distance(st, vec2(0.0));
        smoke = (1.0 - step(0.05, len)) * vec3(1.0);
    }

    // sample neighbors for diffusion (use original texture as "previous state" if you have ping-pong)
    vec2 invRes = 1.0 / uRes;
    vec3 left  = texture(txt, uv - vec2(invRes.x, 0.0)).rgb;
    vec3 right = texture(txt, uv + vec2(invRes.x, 0.0)).rgb;
    vec3 up    = texture(txt, uv - vec2(0.0, invRes.y)).rgb;
    vec3 down  = texture(txt, uv + vec2(0.0, invRes.y)).rgb;
    vec3 center = texture(txt, uv).rgb;

    // noise-driven bias for left-right
    float n = fbm(p * 2.0 + vec2(0.0, -uTime * 0.8));
    float randBias = noiseGradient(p + uTime * 0.2);

    // diffusion coefficient (tweakable)
    float diff = 8.0 * 0.016 * ( (1.0 + randBias) * left.r + (1.0 - randBias) * right.r + down.r + 3.0 * up.r - 6.0 * center.r );
    float minimum = 0.003;
    if (diff >= -minimum && diff < 0.0) diff = -minimum;

    // combine center + extra smoke seed
    vec3 diffuse = center + smoke;

    // compute intensity (single-channel brightness)
    float intensity = diffuse.r + diff;

    // shape mask: make a cone-like flame (narrow at top)
    float cone = smoothstep(0.18, -0.05, abs(p.x) * 1.2 - (uv.y) * 1.6); // tune numbers to adjust cone shape
    intensity *= cone;

    // add vertical falloff so base is brightest
    float verticalFalloff = smoothstep(0.0, 0.9, 1.0 - uv.y);
    intensity *= (0.6 + 0.6 * verticalFalloff);

    // add a noise-driven flicker (small)
    intensity += n * 0.25 * (1.0 - uv.y);

    // clamp intensity
    intensity = clamp(intensity, 0.0, 1.0);

    // base color from palette
    vec3 col = firePalette(intensity);

    // ------------------ sparks ------------------
    // generate a few sparks procedurally (cheap, in-fragment)
    vec3 sparks = vec3(0.0);
    // Parameters
    int SPARK_COUNT = 18;
    for (int i = 0; i < SPARK_COUNT; i++) {
        float fi = float(i);
        // seed depends on index so spark positions are deterministic
        vec2 seed = vec2(fi * 7.13, fi * 13.37);
        // life: 0..1
        float life = fract(uTime * (0.8 + mod(fi,3.0)*0.07) + fi * 0.37);
        // random offset per-spark
        vec2 r = random2(seed + floor(uTime * 3.0));
        // start near torch top (base) and rise upward with some horizontal drift
        float startY = 0.12 + 0.02 * r.y;
        float rise = life * (0.6 + 0.6 * r.x); // how far it rises
        vec2 sp = vec2(0.5 + r.x * (0.06 + life * 0.12), startY + rise);
        // distance from current fragment to spark
        float d = length(uv - sp);
        // spark radius depends on life (young are small, middle bigger, then fade)
        float radius = 0.01 + 0.025 * (1.0 - life);
        float att = smoothstep(radius, 0.0, d);
        // fade by life: sparks brighten then dim
        float fade = pow(1.0 - life, 2.0);
        sparks += vec3(1.0, 0.8, 0.4) * att * fade * 1.2;
    }
    col += sparks;

    // ------------------ glow / ambient light ------------------
    float glow = exp(-6.0 * length((uv - vec2(0.5, 0.12)) * vec2(1.0, 0.6)));
    col += vec3(1.0, 0.45, 0.15) * glow * 0.6;

    // subtle smoke at top (desaturated, rising)
    float smokeNoise = fbm((p + vec2(0.0, uTime * 0.25)) * 1.5);
    float smokeMask = smoothstep(0.5, 0.0, uv.y - 0.6 + smokeNoise * 0.15);
    col = mix(col, vec3(0.08, 0.06, 0.05), smokeMask * 0.35);

    // gamma / tonemapping (soft)
    col = 1.0 - exp(-col * 1.5);
    col = pow(col, vec3(0.95));

    fragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
}

`;

function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error(gl.getShaderInfoLog(shader));
    return null;
  }
  return shader;
}
function createProgram(gl, vs, fs) {
  const v = createShader(gl, gl.VERTEX_SHADER, vs);
  const f = createShader(gl, gl.FRAGMENT_SHADER, fs);
  const prog = gl.createProgram();
  gl.attachShader(prog, v);
  gl.attachShader(prog, f);
  gl.linkProgram(prog);
  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
    console.error(gl.getProgramInfoLog(prog));
    return null;
  }
  return prog;
}

const canvas = document.getElementById("glcanvas");
const gl = canvas.getContext("webgl2");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const program = createProgram(gl, vertexShaderSource, fragmentShaderSource);
gl.useProgram(program);

const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
  -1,-1, 1,-1, -1,1,
  -1,1, 1,-1, 1,1
]), gl.STATIC_DRAW);

const posLoc = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);

const timeLoc = gl.getUniformLocation(program,"u_time");
const resLoc = gl.getUniformLocation(program,"u_resolution");

function render(t){
  gl.viewport(0,0,canvas.width,canvas.height);
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.uniform1f(timeLoc, t*0.001);
  gl.uniform2f(resLoc, canvas.width, canvas.height);

  gl.drawArrays(gl.TRIANGLES, 0, 6);
  requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>
